package org.apache.maven.plugin.javadoc; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import com.thoughtworks.qdox.JavaDocBuilder; import com.thoughtworks.qdox.model.AbstractInheritableJavaEntity; import com.thoughtworks.qdox.model.AbstractJavaEntity; import com.thoughtworks.qdox.model.Annotation; import com.thoughtworks.qdox.model.DocletTag; import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaField; import com.thoughtworks.qdox.model.JavaMethod; import com.thoughtworks.qdox.model.JavaParameter; import com.thoughtworks.qdox.model.Type; import com.thoughtworks.qdox.model.TypeVariable; import com.thoughtworks.qdox.parser.ParseException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ClassUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Settings; import org.apache.maven.shared.invoker.MavenInvocationException; import org.codehaus.plexus.components.interactivity.InputHandler; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.ReaderFactory; import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.util.WriterFactory; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Abstract class to fix Javadoc documentation and tags in source files. * <br/> * See <a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#wheretags">Where Tags * Can Be Used</a>. * * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a> * @version $Id$ * @since 2.6 */ public abstract class AbstractFixJavadocMojo extends AbstractMojo { /** * The vm line separator */ private static final String EOL = System.getProperty( "line.separator" ); /** * Tag name for @author * */ private static final String AUTHOR_TAG = "author"; /** * Tag name for @version * */ private static final String VERSION_TAG = "version"; /** * Tag name for @since * */ private static final String SINCE_TAG = "since"; /** * Tag name for @param * */ private static final String PARAM_TAG = "param"; /** * Tag name for @return * */ private static final String RETURN_TAG = "return"; /** * Tag name for @throws * */ private static final String THROWS_TAG = "throws"; /** * Tag name for @link * */ private static final String LINK_TAG = "link"; /** * Tag name for {@inheritDoc} * */ private static final String INHERITED_TAG = "{@inheritDoc}"; /** * Start Javadoc String i.e. <code>/**</code> * */ private static final String START_JAVADOC = "/**"; /** * End Javadoc String i.e. <code>*/</code> * */ private static final String END_JAVADOC = "*/"; /** * Javadoc Separator i.e. <code> * </code> * */ private static final String SEPARATOR_JAVADOC = " * "; /** * Inherited Javadoc i.e. <code>/** {@inheritDoc} */</code> * */ private static final String INHERITED_JAVADOC = START_JAVADOC + " " + INHERITED_TAG + " " + END_JAVADOC; /** * <code>all</code> parameter used by {@link #fixTags} * */ private static final String FIX_TAGS_ALL = "all"; /** * <code>public</code> parameter used by {@link #level} * */ private static final String LEVEL_PUBLIC = "public"; /** * <code>protected</code> parameter used by {@link #level} * */ private static final String LEVEL_PROTECTED = "protected"; /** * <code>package</code> parameter used by {@link #level} * */ private static final String LEVEL_PACKAGE = "package"; /** * <code>private</code> parameter used by {@link #level} * */ private static final String LEVEL_PRIVATE = "private"; /** * The Clirr Maven plugin groupId <code>org.codehaus.mojo</code> * */ private static final String CLIRR_MAVEN_PLUGIN_GROUPID = "org.codehaus.mojo"; /** * The Clirr Maven plugin artifactId <code>clirr-maven-plugin</code> * */ private static final String CLIRR_MAVEN_PLUGIN_ARTIFACTID = "clirr-maven-plugin"; /** * The latest Clirr Maven plugin version <code>2.2.2</code> * */ private static final String CLIRR_MAVEN_PLUGIN_VERSION = "2.2.2"; /** * The Clirr Maven plugin goal <code>check</code> * */ private static final String CLIRR_MAVEN_PLUGIN_GOAL = "check"; /** * Java Files Pattern. */ public static final String JAVA_FILES = "**\\/*.java"; /** * Default version value. */ public static final String DEFAULT_VERSION_VALUE = "\u0024Id: \u0024Id"; // ---------------------------------------------------------------------- // Mojo components // ---------------------------------------------------------------------- /** * Input handler, needed for command line handling. */ @Component private InputHandler inputHandler; // ---------------------------------------------------------------------- // Mojo parameters // ---------------------------------------------------------------------- /** * Version to compare the current code against using the * <a href="http://mojo.codehaus.org/clirr-maven-plugin/">Clirr Maven Plugin</a>. * <br/> * See <a href="#defaultSince">defaultSince</a>. */ @Parameter ( property = "comparisonVersion", defaultValue = "(,${project.version})" ) private String comparisonVersion; /** * Default value for the Javadoc tag <code>@author</code>. * <br/> * If not specified, the <code>user.name</code> defined in the System properties will be used. */ @Parameter ( property = "defaultAuthor" ) private String defaultAuthor; /** * Default value for the Javadoc tag <code>@since</code>. */ @Parameter ( property = "defaultSince", defaultValue = "${project.version}" ) private String defaultSince; /** * Default value for the Javadoc tag <code>@version</code>. * <br/> * By default, it is <code>$Id:$</code>, corresponding to a * <a href="http://svnbook.red-bean.com/en/1.1/ch07s02.html#svn-ch-7-sect-2.3.4">SVN keyword</a>. * Refer to your SCM to use an other SCM keyword. */ @Parameter ( property = "defaultVersion", defaultValue = DEFAULT_VERSION_VALUE ) private String defaultVersion = "\u0024Id: \u0024"; // can't use default-value="\u0024Id: \u0024" /** * The file encoding to use when reading the source files. If the property * <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used. */ @Parameter ( property = "encoding", defaultValue = "${project.build.sourceEncoding}" ) private String encoding; /** * Comma separated excludes Java files, i.e. <code>**/*Test.java</code>. */ @Parameter ( property = "excludes" ) private String excludes; /** * Comma separated tags to fix in classes, interfaces or methods Javadoc comments. * Possible values are: * <ul> * <li>all (fix all Javadoc tags)</li> * <li>author (fix only @author tag)</li> * <li>version (fix only @version tag)</li> * <li>since (fix only @since tag)</li> * <li>param (fix only @param tag)</li> * <li>return (fix only @return tag)</li> * <li>throws (fix only @throws tag)</li> * <li>link (fix only @link tag)</li> * </ul> */ @Parameter ( property = "fixTags", defaultValue = "all" ) private String fixTags; /** * Flag to fix the classes or interfaces Javadoc comments according the <code>level</code>. */ @Parameter ( property = "fixClassComment", defaultValue = "true" ) private boolean fixClassComment; /** * Flag to fix the fields Javadoc comments according the <code>level</code>. */ @Parameter ( property = "fixFieldComment", defaultValue = "true" ) private boolean fixFieldComment; /** * Flag to fix the methods Javadoc comments according the <code>level</code>. */ @Parameter ( property = "fixMethodComment", defaultValue = "true" ) private boolean fixMethodComment; /** * Flag to remove throws tags from unknown classes. */ @Parameter ( property = "removeUnknownThrows", defaultValue = "false" ) private boolean removeUnknownThrows; /** * Forcing the goal execution i.e. skip warranty messages (not recommended). */ @Parameter ( property = "force" ) private boolean force; /** * Flag to ignore or not Clirr. */ @Parameter ( property = "ignoreClirr", defaultValue = "false" ) protected boolean ignoreClirr; /** * Comma separated includes Java files, i.e. <code>**/*Test.java</code>. * <p/> * <strong>Note:</strong> default value is {@code **\/*.java}. */ @Parameter ( property = "includes", defaultValue = JAVA_FILES ) private String includes; /** * Specifies the access level for classes and members to show in the Javadocs. * Possible values are: * <ul> * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#public">public</a> * (shows only public classes and members)</li> * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#protected">protected</a> * (shows only public and protected classes and members)</li> * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#package">package</a> * (shows all classes and members not marked private)</li> * <li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#private">private</a> * (shows all classes and members)</li> * </ul> */ @Parameter ( property = "level", defaultValue = "protected" ) private String level; /** * The local repository where the artifacts are located, used by the tests. */ @Parameter ( property = "localRepository" ) private ArtifactRepository localRepository; /** * Output directory where Java classes will be rewritten. */ @Parameter ( property = "outputDirectory", defaultValue = "${project.build.sourceDirectory}" ) private File outputDirectory; /** * The Maven Project Object. */ @Parameter( defaultValue = "${project}", readonly = true, required = true ) private MavenProject project; /** * The current user system settings for use in Maven. */ @Parameter( defaultValue = "${settings}", readonly = true, required = true ) private Settings settings; // ---------------------------------------------------------------------- // Internal fields // ---------------------------------------------------------------------- /** * The current project class loader. */ private ClassLoader projectClassLoader; /** * Split {@link #fixTags} by comma. * * @see {@link #init()} */ private String[] fixTagsSplitted; /** * New classes found by Clirr. */ private List<String> clirrNewClasses; /** * New Methods in a Class (the key) found by Clirr. */ private Map<String, List<String>> clirrNewMethods; /** * List of classes where <code>*since</code> is added. Will be used to add or not this tag in the methods. */ private List<String> sinceClasses; /** * {@inheritDoc} */ public void execute() throws MojoExecutionException, MojoFailureException { if ( !fixClassComment && !fixFieldComment && !fixMethodComment ) { getLog().info( "Specified to NOT fix classes, fields and methods. Nothing to do." ); return; } // verify goal params init(); if ( fixTagsSplitted.length == 0 ) { getLog().info( "No fix tag specified. Nothing to do." ); return; } // add warranty msg if ( !preCheck() ) { return; } // run clirr try { executeClirr(); } catch ( MavenInvocationException e ) { if ( getLog().isDebugEnabled() ) { getLog().error( "MavenInvocationException: " + e.getMessage(), e ); } else { getLog().error( "MavenInvocationException: " + e.getMessage() ); } getLog().info( "Clirr is ignored." ); } // run qdox and process try { JavaClass[] javaClasses = getQdoxClasses(); if ( javaClasses != null ) { for ( JavaClass javaClass : javaClasses ) { processFix( javaClass ); } } } catch ( IOException e ) { throw new MojoExecutionException( "IOException: " + e.getMessage(), e ); } } // ---------------------------------------------------------------------- // protected methods // ---------------------------------------------------------------------- /** * @param p not null maven project. * @return the artifact type. */ protected String getArtifactType( MavenProject p ) { return p.getArtifact().getType(); } /** * @param p not null maven project. * @return the list of source paths for the given project. */ protected List<String> getProjectSourceRoots( MavenProject p ) { return ( p.getCompileSourceRoots() == null ? Collections.<String>emptyList() : new LinkedList<String>( p.getCompileSourceRoots() ) ); } /** * @param p not null * @return the compile classpath elements * @throws DependencyResolutionRequiredException * if any */ protected List<String> getCompileClasspathElements( MavenProject p ) throws DependencyResolutionRequiredException { return ( p.getCompileClasspathElements() == null ? Collections.<String>emptyList() : new LinkedList<String>( p.getCompileClasspathElements() ) ); } /** * @param javaMethod not null * @return the fully qualify name of javaMethod with signature */ protected static String getJavaMethodAsString( JavaMethod javaMethod ) { return javaMethod.getParentClass().getFullyQualifiedName() + "#" + javaMethod.getCallSignature(); } // ---------------------------------------------------------------------- // private methods // ---------------------------------------------------------------------- /** * Init goal parameters. */ private void init() { // defaultAuthor if ( StringUtils.isEmpty( defaultAuthor ) ) { defaultAuthor = System.getProperty( "user.name" ); } // defaultSince int i = defaultSince.indexOf( "-" + Artifact.SNAPSHOT_VERSION ); if ( i != -1 ) { defaultSince = defaultSince.substring( 0, i ); } // fixTags if ( !FIX_TAGS_ALL.equalsIgnoreCase( fixTags.trim() ) ) { String[] split = StringUtils.split( fixTags, "," ); List<String> filtered = new LinkedList<String>(); for ( String aSplit : split ) { String s = aSplit.trim(); if ( JavadocUtil.equalsIgnoreCase( s, FIX_TAGS_ALL, AUTHOR_TAG, VERSION_TAG, SINCE_TAG, PARAM_TAG, THROWS_TAG, LINK_TAG, RETURN_TAG ) ) { filtered.add( s ); } else { if ( getLog().isWarnEnabled() ) { getLog().warn( "Unrecognized '" + s + "' for fixTags parameter. Ignored it!" ); } } } fixTags = StringUtils.join( filtered.iterator(), "," ); } fixTagsSplitted = StringUtils.split( fixTags, "," ); // encoding if ( StringUtils.isEmpty( encoding ) ) { if ( getLog().isWarnEnabled() ) { getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING + ", i.e. build is platform dependent!" ); } encoding = ReaderFactory.FILE_ENCODING; } // level level = level.trim(); if ( !JavadocUtil.equalsIgnoreCase( level, LEVEL_PUBLIC, LEVEL_PROTECTED, LEVEL_PACKAGE, LEVEL_PRIVATE ) ) { if ( getLog().isWarnEnabled() ) { getLog().warn( "Unrecognized '" + level + "' for level parameter, using 'protected' level." ); } level = "protected"; } } /** * @return <code>true</code> if the user wants to proceed, <code>false</code> otherwise. * @throws MojoExecutionException if any */ private boolean preCheck() throws MojoExecutionException { if ( force ) { return true; } if ( outputDirectory != null && !outputDirectory.getAbsolutePath().equals( getProjectSourceDirectory().getAbsolutePath() ) ) { return true; } if ( !settings.isInteractiveMode() ) { getLog().error( "Maven is not attempt to interact with the user for input. " + "Verify the <interactiveMode/> configuration in your settings." ); return false; } getLog().warn( "" ); getLog().warn( " WARRANTY DISCLAIMER" ); getLog().warn( "" ); getLog().warn( "All warranties with regard to this Maven goal are disclaimed!" ); getLog().warn( "The changes will be done directly in the source code." ); getLog().warn( "The Maven Team strongly recommends the use of a SCM software BEFORE executing this goal." ); getLog().warn( "" ); while ( true ) { getLog().info( "Are you sure to proceed? [Y]es [N]o" ); try { String userExpression = inputHandler.readLine(); if ( userExpression == null || JavadocUtil.equalsIgnoreCase( userExpression, "Y", "Yes" ) ) { getLog().info( "OK, let's proceed..." ); break; } if ( JavadocUtil.equalsIgnoreCase( userExpression, "N", "No" ) ) { getLog().info( "No changes in your sources occur." ); return false; } } catch ( IOException e ) { throw new MojoExecutionException( "Unable to read from standard input.", e ); } } return true; } /** * @return the source dir as File for the given project */ private File getProjectSourceDirectory() { return new File( project.getBuild().getSourceDirectory() ); } /** * Invoke Maven to run clirr-maven-plugin to find API differences. * * @throws MavenInvocationException if any */ private void executeClirr() throws MavenInvocationException { if ( ignoreClirr ) { getLog().info( "Clirr is ignored." ); return; } String clirrGoal = getFullClirrGoal(); // http://mojo.codehaus.org/clirr-maven-plugin/check-mojo.html File clirrTextOutputFile = FileUtils.createTempFile( "clirr", ".txt", new File( project.getBuild().getDirectory() ) ); Properties properties = new Properties(); properties.put( "textOutputFile", clirrTextOutputFile.getAbsolutePath() ); properties.put( "comparisonVersion", comparisonVersion ); properties.put( "failOnError", "false" ); File invokerDir = new File( project.getBuild().getDirectory(), "invoker" ); invokerDir.mkdirs(); File invokerLogFile = FileUtils.createTempFile( "clirr-maven-plugin", ".txt", invokerDir ); new File( project.getBuild().getDirectory(), "invoker-clirr-maven-plugin.txt" ); JavadocUtil.invokeMaven( getLog(), new File( localRepository.getBasedir() ), project.getFile(), Collections.singletonList( clirrGoal ), properties, invokerLogFile ); try { if ( invokerLogFile.exists() ) { String invokerLogContent = StringUtils.unifyLineSeparators( FileUtils.fileRead( invokerLogFile, "UTF-8" ) ); // see org.codehaus.mojo.clirr.AbstractClirrMojo#getComparisonArtifact() final String artifactNotFoundMsg = "Unable to find a previous version of the project in the repository"; if ( invokerLogContent.contains( artifactNotFoundMsg ) ) { getLog().warn( "No previous artifact has been deployed, Clirr is ignored." ); return; } } } catch ( IOException e ) { getLog().debug( "IOException: " + e.getMessage() ); } try { parseClirrTextOutputFile( clirrTextOutputFile ); } catch ( IOException e ) { if ( getLog().isDebugEnabled() ) { getLog().debug( "IOException: " + e.getMessage(), e ); } getLog().info( "IOException when parsing Clirr output '" + clirrTextOutputFile.getAbsolutePath() + "', Clirr is ignored." ); } } /** * @param clirrTextOutputFile not null * @throws IOException if any */ private void parseClirrTextOutputFile( File clirrTextOutputFile ) throws IOException { if ( !clirrTextOutputFile.exists() ) { if ( getLog().isInfoEnabled() ) { getLog().info( "No Clirr output file '" + clirrTextOutputFile.getAbsolutePath() + "' exists, Clirr is ignored." ); } return; } if ( getLog().isInfoEnabled() ) { getLog().info( "Clirr output file was created: " + clirrTextOutputFile.getAbsolutePath() ); } clirrNewClasses = new LinkedList<String>(); clirrNewMethods = new LinkedHashMap<String, List<String>>(); BufferedReader reader = null; try { reader = new BufferedReader( ReaderFactory.newReader( clirrTextOutputFile, "UTF-8" ) ); for ( String line = reader.readLine(); line != null; line = reader.readLine() ) { String[] split = StringUtils.split( line, ":" ); if ( split.length != 4 ) { if ( getLog().isDebugEnabled() ) { getLog().debug( "Unable to parse the clirr line: " + line ); } continue; } int code; try { code = Integer.parseInt( split[1].trim() ); } catch ( NumberFormatException e ) { if ( getLog().isDebugEnabled() ) { getLog().debug( "Unable to parse the clirr line: " + line ); } continue; } // http://clirr.sourceforge.net/clirr-core/exegesis.html // 7011 - Method Added // 7012 - Method Added to Interface // 8000 - Class Added List<String> list; String[] splits2; // CHECKSTYLE_OFF: MagicNumber switch ( code ) { case 7011: list = clirrNewMethods.get( split[2].trim() ); if ( list == null ) { list = new ArrayList<String>(); } splits2 = StringUtils.split( split[3].trim(), "'" ); if ( splits2.length != 3 ) { continue; } list.add( splits2[1].trim() ); clirrNewMethods.put( split[2].trim(), list ); break; case 7012: list = clirrNewMethods.get( split[2].trim() ); if ( list == null ) { list = new ArrayList<String>(); } splits2 = StringUtils.split( split[3].trim(), "'" ); if ( splits2.length != 3 ) { continue; } list.add( splits2[1].trim() ); clirrNewMethods.put( split[2].trim(), list ); break; case 8000: clirrNewClasses.add( split[2].trim() ); break; default: break; } // CHECKSTYLE_ON: MagicNumber } reader.close(); reader = null; } finally { IOUtils.closeQuietly( reader ); } if ( clirrNewClasses.isEmpty() && clirrNewMethods.isEmpty() ) { getLog().info( "Clirr NOT found API differences." ); } else { getLog().info( "Clirr found API differences, i.e. new classes/interfaces or methods." ); } } /** * @param tag not null * @return <code>true</code> if <code>tag</code> is defined in {@link #fixTags}. */ private boolean fixTag( String tag ) { if ( fixTagsSplitted.length == 1 && fixTagsSplitted[0].equals( FIX_TAGS_ALL ) ) { return true; } for ( String aFixTagsSplitted : fixTagsSplitted ) { if ( aFixTagsSplitted.trim().equals( tag ) ) { return true; } } return false; } /** * Calling Qdox to find {@link JavaClass} objects from the Maven project sources. * Ignore java class if Qdox has parsing errors. * * @return an array of {@link JavaClass} found by QDox * @throws IOException if any * @throws MojoExecutionException if any */ private JavaClass[] getQdoxClasses() throws IOException, MojoExecutionException { if ( "pom".equalsIgnoreCase( project.getPackaging() ) ) { getLog().warn( "This project has 'pom' packaging, no Java sources is available." ); return null; } List<File> javaFiles = new LinkedList<File>(); for ( String sourceRoot : getProjectSourceRoots( project ) ) { File f = new File( sourceRoot ); if ( f.isDirectory() ) { javaFiles.addAll( FileUtils.getFiles( f, includes, excludes, true ) ); } else { if ( getLog().isWarnEnabled() ) { getLog().warn( f + " doesn't exist. Ignored it." ); } } } JavaDocBuilder builder = new JavaDocBuilder(); builder.getClassLibrary().addClassLoader( getProjectClassLoader() ); builder.setEncoding( encoding ); for ( File f : javaFiles ) { if ( !f.getAbsolutePath().toLowerCase( Locale.ENGLISH ).endsWith( ".java" ) && getLog().isWarnEnabled() ) { getLog().warn( "'" + f + "' is not a Java file. Ignored it." ); continue; } try { builder.addSource( f ); } catch ( ParseException e ) { if ( getLog().isWarnEnabled() ) { getLog().warn( "QDOX ParseException: " + e.getMessage() + ". Can't fix it." ); } } } return builder.getClasses(); } /** * @return the classLoader for the given project using lazy instantiation. * @throws MojoExecutionException if any */ private ClassLoader getProjectClassLoader() throws MojoExecutionException { if ( projectClassLoader == null ) { List<String> classPath; try { classPath = getCompileClasspathElements( project ); } catch ( DependencyResolutionRequiredException e ) { throw new MojoExecutionException( "DependencyResolutionRequiredException: " + e.getMessage(), e ); } List<URL> urls = new ArrayList<URL>( classPath.size() ); for ( String filename : classPath ) { try { urls.add( new File( filename ).toURL() ); } catch ( MalformedURLException e ) { throw new MojoExecutionException( "MalformedURLException: " + e.getMessage(), e ); } } projectClassLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), null ); } return projectClassLoader; } /** * Process the given {@link JavaClass}, ie add missing javadoc tags depending user parameters. * * @param javaClass not null * @throws IOException if any * @throws MojoExecutionException if any */ private void processFix( JavaClass javaClass ) throws IOException, MojoExecutionException { // Skipping inner classes if ( javaClass.isInner() ) { return; } File javaFile = new File( javaClass.getSource().getURL().getFile() ); // the original java content in memory final String originalContent = StringUtils.unifyLineSeparators( FileUtils.fileRead( javaFile, encoding ) ); if ( getLog().isDebugEnabled() ) { getLog().debug( "Analyzing " + javaClass.getFullyQualifiedName() ); } final StringWriter stringWriter = new StringWriter(); BufferedReader reader = null; boolean changeDetected = false; try { reader = new BufferedReader( new StringReader( originalContent ) ); int lineNumber = 0; for ( String line = reader.readLine(); line != null; line = reader.readLine() ) { lineNumber++; final String indent = autodetectIndentation( line ); // fixing classes if ( javaClass.getComment() == null && javaClass.getAnnotations() != null && javaClass.getAnnotations().length != 0 ) { if ( lineNumber == javaClass.getAnnotations()[0].getLineNumber() ) { changeDetected |= fixClassComment( stringWriter, originalContent, javaClass, indent ); takeCareSingleComment( stringWriter, originalContent, javaClass ); } } else { if ( lineNumber == javaClass.getLineNumber() ) { changeDetected |= fixClassComment( stringWriter, originalContent, javaClass, indent ); takeCareSingleComment( stringWriter, originalContent, javaClass ); } } // fixing fields if ( javaClass.getFields() != null ) { for ( JavaField field : javaClass.getFields() ) { if ( lineNumber == field.getLineNumber() ) { changeDetected |= fixFieldComment( stringWriter, javaClass, field, indent ); } } } // fixing methods if ( javaClass.getMethods() != null ) { for ( JavaMethod method : javaClass.getMethods() ) { if ( lineNumber == method.getLineNumber() ) { changeDetected |= fixMethodComment( stringWriter, originalContent, method, indent ); takeCareSingleComment( stringWriter, originalContent, method ); } } } stringWriter.write( line ); stringWriter.write( EOL ); } reader.close(); reader = null; } finally { IOUtil.close( reader ); } if ( changeDetected ) { if ( getLog().isInfoEnabled() ) { getLog().info( "Saving changes to " + javaClass.getFullyQualifiedName() ); } if ( outputDirectory != null && !outputDirectory.getAbsolutePath().equals( getProjectSourceDirectory().getAbsolutePath() ) ) { String path = StringUtils.replace( javaFile.getAbsolutePath().replaceAll( "\\\\", "/" ), project.getBuild().getSourceDirectory().replaceAll( "\\\\", "/" ), "" ); javaFile = new File( outputDirectory, path ); javaFile.getParentFile().mkdirs(); } writeFile( javaFile, encoding, stringWriter.toString() ); } else { if ( getLog().isDebugEnabled() ) { getLog().debug( "No changes made to " + javaClass.getFullyQualifiedName() ); } } } /** * Take care of block or single comments between Javadoc comment and entity declaration ie: * <br/> * <code> * <font color="#808080">1</font> <font color="#ffffff"> </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">2</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* {Javadoc Comment}</font><br /> * <font color="#808080">3</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">4</font> <font color="#ffffff"> </font> * <font color="#3f7f5f">/*</font><br /> * <font color="#808080">5</font> <font color="#ffffff">  </font> * <font color="#3f7f5f">* {Block Comment}</font><br /> * <font color="#808080">6</font> <font color="#ffffff">  </font> * <font color="#3f7f5f">*/</font><br /> * <font color="#808080">7</font> <font color="#ffffff"> </font> * <font color="#3f7f5f">// {Single comment}</font><br /> * <font color="#808080">8</font> <font color="#ffffff"> </font> * <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> * <font color="#000000">dummyMethod</font><font color="#000000">( </font> * <font color="#000000">String s </font><font color="#000000">){}</font> * </code> * * @param stringWriter not null * @param originalContent not null * @param entity not null * @param changeDetected * @return the updated changeDetected flag * @throws IOException if any * @see #extractOriginalJavadoc(String, AbstractJavaEntity) */ private void takeCareSingleComment( final StringWriter stringWriter, final String originalContent, final AbstractInheritableJavaEntity entity ) throws IOException { if ( entity.getComment() == null ) { return; } String javadocComment = trimRight( extractOriginalJavadoc( originalContent, entity ) ); String extraComment = javadocComment.substring( javadocComment.indexOf( END_JAVADOC ) + END_JAVADOC.length() ); if ( StringUtils.isNotEmpty( extraComment ) ) { if ( extraComment.contains( EOL ) ) { stringWriter.write( extraComment.substring( extraComment.indexOf( EOL ) + EOL.length() ) ); } else { stringWriter.write( extraComment ); } stringWriter.write( EOL ); } } /** * Add/update Javadoc class comment. * * @param stringWriter * @param originalContent * @param javaClass * @param indent * @return {@code true} if the comment is updated, otherwise {@code false} * @throws MojoExecutionException * @throws IOException */ private boolean fixClassComment( final StringWriter stringWriter, final String originalContent, final JavaClass javaClass, final String indent ) throws MojoExecutionException, IOException { if ( !fixClassComment ) { return false; } if ( !isInLevel( javaClass.getModifiers() ) ) { return false; } // add if ( javaClass.getComment() == null ) { addDefaultClassComment( stringWriter, javaClass, indent ); return true; } // update return updateEntityComment( stringWriter, originalContent, javaClass, indent ); } /** * @param modifiers list of modifiers (public, private, protected, package) * @return <code>true</code> if modifier is align with <code>level</code>. */ private boolean isInLevel( String[] modifiers ) { List<String> modifiersAsList = Arrays.asList( modifiers ); if ( LEVEL_PUBLIC.equalsIgnoreCase( level.trim() ) ) { return modifiersAsList.contains( LEVEL_PUBLIC ); } if ( LEVEL_PROTECTED.equalsIgnoreCase( level.trim() ) ) { return ( modifiersAsList.contains( LEVEL_PUBLIC ) || modifiersAsList.contains( LEVEL_PROTECTED ) ); } if ( LEVEL_PACKAGE.equalsIgnoreCase( level.trim() ) ) { return !modifiersAsList.contains( LEVEL_PRIVATE ); } // should be private (shows all classes and members) return true; } /** * Add a default Javadoc for the given class, i.e.: * <br/> * <code> * <font color="#808080">1</font> <font color="#ffffff"> </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">2</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* {Comment based on the class name}</font><br /> * <font color="#808080">3</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">*</font><br /> * <font color="#808080">4</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@author </font> * <font color="#3f5fbf">X {added if addMissingAuthor}</font><br /> * <font color="#808080">5</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@version </font> * <font color="#3f5fbf">X {added if addMissingVersion}</font><br /> * <font color="#808080">6</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@since </font> * <font color="#3f5fbf">X {added if addMissingSince and new classes * from previous version}</font><br /> * <font color="#808080">7</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">8</font> <font color="#7f0055"><b>public class </b></font> * <font color="#000000">DummyClass </font><font color="#000000">{}</font></code> * </code> * * @param buffer not null * @param javaClass not null * @param indent not null * @see #getDefaultClassJavadocComment(JavaClass) * @see #appendDefaultAuthorTag(StringBuilder, String) * @see #appendDefaultSinceTag(StringBuilder, String) * @see #appendDefaultVersionTag(StringBuilder, String) */ private void addDefaultClassComment( final StringWriter stringWriter, final JavaClass javaClass, final String indent ) { StringBuilder sb = new StringBuilder(); sb.append( indent ).append( START_JAVADOC ); sb.append( EOL ); sb.append( indent ).append( SEPARATOR_JAVADOC ); sb.append( getDefaultClassJavadocComment( javaClass ) ); sb.append( EOL ); appendSeparator( sb, indent ); appendDefaultAuthorTag( sb, indent ); appendDefaultVersionTag( sb, indent ); if ( fixTag( SINCE_TAG ) ) { if ( !ignoreClirr ) { if ( isNewClassFromLastVersion( javaClass ) ) { appendDefaultSinceTag( sb, indent ); } } else { appendDefaultSinceTag( sb, indent ); addSinceClasses( javaClass ); } } sb.append( indent ).append( " " ).append( END_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); } /** * Add Javadoc field comment, only for static fields or interface fields. * * @param stringWriter not null * @param javaClass not null * @param field not null * @param indent not null * @return {@code true} if comment was updated, otherwise {@code false} * @throws IOException if any */ private boolean fixFieldComment( final StringWriter stringWriter, final JavaClass javaClass, final JavaField field, final String indent ) throws IOException { if ( !fixFieldComment ) { return false; } if ( !javaClass.isInterface() && ( !isInLevel( field.getModifiers() ) || !field.isStatic() ) ) { return false; } // add if ( field.getComment() == null ) { addDefaultFieldComment( stringWriter, field, indent ); return true; } // no update return false; } /** * Add a default Javadoc for the given field, i.e.: * <br/> * <code> * <font color="#808080">1</font> <font color="#ffffff">    </font> * <font color="#3f5fbf">/** Constant </font><font color="#7f7f9f"><code></font> * <font color="#3f5fbf">MY_STRING_CONSTANT="value"</font> * <font color="#7f7f9f"></code> </font><font color="#3f5fbf">*/</font><br /> * <font color="#808080">2</font> <font color="#ffffff">    </font> * <font color="#7f0055"><b>public static final </b></font> * <font color="#000000">String MY_STRING_CONSTANT = </font> * <font color="#2a00ff">"value"</font><font color="#000000">;</font> * </code> * * @param stringWriter not null * @param field not null * @param indent not null * @throws IOException if any */ private void addDefaultFieldComment( final StringWriter stringWriter, final JavaField field, final String indent ) throws IOException { StringBuilder sb = new StringBuilder(); sb.append( indent ).append( START_JAVADOC ).append( " " ); sb.append( "Constant <code>" ).append( field.getName() ); if ( StringUtils.isNotEmpty( field.getInitializationExpression() ) ) { String qualifiedName = field.getType().getJavaClass().getFullyQualifiedName(); if ( qualifiedName.equals( Byte.TYPE.toString() ) || qualifiedName.equals( Short.TYPE.toString() ) || qualifiedName.equals( Integer.TYPE.toString() ) || qualifiedName.equals( Long.TYPE.toString() ) || qualifiedName.equals( Float.TYPE.toString() ) || qualifiedName.equals( Double.TYPE.toString() ) || qualifiedName.equals( Boolean.TYPE.toString() ) || qualifiedName.equals( Character.TYPE.toString() ) ) { sb.append( "=" ); sb.append( field.getInitializationExpression().trim() ); } if ( qualifiedName.equals( String.class.getName() ) ) { StringBuilder value = new StringBuilder(); String[] lines = getLines( field.getInitializationExpression() ); for ( String line : lines ) { StringTokenizer token = new StringTokenizer( line.trim(), "\"\n\r" ); while ( token.hasMoreTokens() ) { String s = token.nextToken(); if ( s.trim().equals( "+" ) ) { continue; } if ( s.trim().endsWith( "\\" ) ) { s += "\""; } value.append( s ); } } sb.append( "=\"" ); // reduce the size // CHECKSTYLE_OFF: MagicNumber if ( value.length() < 40 ) { sb.append( value.toString() ).append( "\"" ); } else { sb.append( value.toString().substring( 0, 39 ) ).append( "\"{trunked}" ); } // CHECKSTYLE_ON: MagicNumber } } sb.append( "</code> " ).append( END_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); } /** * Add/update Javadoc method comment. * * @param stringWriter not null * @param originalContent not null * @param javaMethod not null * @param indent not null * @return {@code true} if comment was updated, otherwise {@code false} * @throws MojoExecutionException if any * @throws IOException if any */ private boolean fixMethodComment( final StringWriter stringWriter, final String originalContent, final JavaMethod javaMethod, final String indent ) throws MojoExecutionException, IOException { if ( !fixMethodComment ) { return false; } if ( !javaMethod.getParentClass().isInterface() && !isInLevel( javaMethod.getModifiers() ) ) { return false; } // add if ( javaMethod.getComment() == null ) { addDefaultMethodComment( stringWriter, javaMethod, indent ); return true; } // update return updateEntityComment( stringWriter, originalContent, javaMethod, indent ); } /** * Add in the buffer a default Javadoc for the given class: * <br/> * <code> * <font color="#808080">1</font> <font color="#ffffff"> </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">2</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* {Comment based on the method name}</font><br /> * <font color="#808080">3</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">*</font><br /> * <font color="#808080">4</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">X {added if addMissingParam}</font><br /> * <font color="#808080">5</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@return </font> * <font color="#3f5fbf">X {added if addMissingReturn}</font><br /> * <font color="#808080">6</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@throws </font> * <font color="#3f5fbf">X {added if addMissingThrows}</font><br /> * <font color="#808080">7</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@since </font> * <font color="#3f5fbf">X {added if addMissingSince and new classes * from previous version}</font><br /> * <font color="#808080">8</font> <font color="#ffffff">  </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">9</font> <font color="#7f0055"><b>public </b></font> * <font color="#7f0055"><b>void </b></font><font color="#000000">dummyMethod</font> * <font color="#000000">( </font><font color="#000000">String s </font> * <font color="#000000">){}</font> * </code> * * @param buffer not null * @param javaMethod not null * @param indent not null * @throws MojoExecutionException if any * @see #getDefaultMethodJavadocComment(JavaMethod) * @see #appendDefaultSinceTag(StringBuilder, String) */ private void addDefaultMethodComment( final StringWriter stringWriter, final JavaMethod javaMethod, final String indent ) throws MojoExecutionException { StringBuilder sb = new StringBuilder(); // special case if ( isInherited( javaMethod ) ) { sb.append( indent ).append( INHERITED_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); return; } sb.append( indent ).append( START_JAVADOC ); sb.append( EOL ); sb.append( indent ).append( SEPARATOR_JAVADOC ); sb.append( getDefaultMethodJavadocComment( javaMethod ) ); sb.append( EOL ); boolean separatorAdded = false; if ( fixTag( PARAM_TAG ) ) { if ( javaMethod.getParameters() != null ) { for ( int i = 0; i < javaMethod.getParameters().length; i++ ) { JavaParameter javaParameter = javaMethod.getParameters()[i]; separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, javaParameter ); } } // is generic? if ( javaMethod.getTypeParameters() != null ) { for ( int i = 0; i < javaMethod.getTypeParameters().length; i++ ) { TypeVariable typeParam = javaMethod.getTypeParameters()[i]; separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, typeParam ); } } } if ( fixTag( RETURN_TAG ) && javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() ) { separatorAdded = appendDefaultReturnTag( sb, indent, separatorAdded, javaMethod ); } if ( fixTag( THROWS_TAG ) && javaMethod.getExceptions() != null && javaMethod.getExceptions().length > 0 ) { for ( int i = 0; i < javaMethod.getExceptions().length; i++ ) { Type exception = javaMethod.getExceptions()[i]; separatorAdded = appendDefaultThrowsTag( sb, indent, separatorAdded, exception ); } } if ( fixTag( SINCE_TAG ) && isNewMethodFromLastRevision( javaMethod ) ) { separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded ); } sb.append( indent ).append( " " ).append( END_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); } /** * @param stringWriter not null * @param originalContent not null * @param entity not null * @param indent not null * @param changeDetected * @return the updated changeDetected flag * @throws MojoExecutionException if any * @throws IOException if any */ private boolean updateEntityComment( final StringWriter stringWriter, final String originalContent, final AbstractInheritableJavaEntity entity, final String indent ) throws MojoExecutionException, IOException { boolean changeDetected = false; String old = null; String s = stringWriter.toString(); int i = s.lastIndexOf( START_JAVADOC ); if ( i != -1 ) { String tmp = s.substring( 0, i ); if ( tmp.lastIndexOf( EOL ) != -1 ) { tmp = tmp.substring( 0, tmp.lastIndexOf( EOL ) ); } old = stringWriter.getBuffer().substring( i ); stringWriter.getBuffer().delete( 0, stringWriter.getBuffer().length() ); stringWriter.write( tmp ); stringWriter.write( EOL ); } else { changeDetected = true; } updateJavadocComment( stringWriter, originalContent, entity, indent ); if ( changeDetected ) { return true; // return now if we already know there's a change } return !stringWriter.getBuffer().substring( i ).equals( old ); } /** * @param stringWriter not null * @param originalContent not null * @param entity not null * @param indent not null * @throws MojoExecutionException if any * @throws IOException if any */ private void updateJavadocComment( final StringWriter stringWriter, final String originalContent, final AbstractInheritableJavaEntity entity, final String indent ) throws MojoExecutionException, IOException { if ( entity.getComment() == null && ( entity.getTags() == null || entity.getTags().length == 0 ) ) { return; } boolean isJavaMethod = false; if ( entity instanceof JavaMethod ) { isJavaMethod = true; } StringBuilder sb = new StringBuilder(); // special case for inherited method if ( isJavaMethod ) { JavaMethod javaMethod = (JavaMethod) entity; if ( isInherited( javaMethod ) ) { // QDOX-154 could be empty if ( StringUtils.isEmpty( javaMethod.getComment() ) ) { sb.append( indent ).append( INHERITED_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); return; } String javadoc = getJavadocComment( originalContent, javaMethod ); // case: /** {@inheritDoc} */ or no tags if ( hasInheritedTag( javadoc ) && ( javaMethod.getTags() == null || javaMethod.getTags().length == 0 ) ) { sb.append( indent ).append( INHERITED_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); return; } if ( javadoc.contains( START_JAVADOC ) ) { javadoc = javadoc.substring( javadoc.indexOf( START_JAVADOC ) + START_JAVADOC.length() ); } if ( javadoc.contains( END_JAVADOC ) ) { javadoc = javadoc.substring( 0, javadoc.indexOf( END_JAVADOC ) ); } sb.append( indent ).append( START_JAVADOC ); sb.append( EOL ); if ( !javadoc.contains( INHERITED_TAG ) ) { sb.append( indent ).append( SEPARATOR_JAVADOC ).append( INHERITED_TAG ); sb.append( EOL ); appendSeparator( sb, indent ); } javadoc = removeLastEmptyJavadocLines( javadoc ); javadoc = alignIndentationJavadocLines( javadoc, indent ); sb.append( javadoc ); sb.append( EOL ); if ( javaMethod.getTags() != null ) { for ( int i = 0; i < javaMethod.getTags().length; i++ ) { DocletTag docletTag = javaMethod.getTags()[i]; // Voluntary ignore these tags if ( JavadocUtil.equals( docletTag.getName(), PARAM_TAG, RETURN_TAG, THROWS_TAG ) ) { continue; } String s = getJavadocComment( originalContent, entity, docletTag ); s = removeLastEmptyJavadocLines( s ); s = alignIndentationJavadocLines( s, indent ); sb.append( s ); sb.append( EOL ); } } sb.append( indent ).append( " " ).append( END_JAVADOC ); sb.append( EOL ); if ( hasInheritedTag( sb.toString().trim() ) ) { sb = new StringBuilder(); sb.append( indent ).append( INHERITED_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); return; } stringWriter.write( sb.toString() ); return; } } sb.append( indent ).append( START_JAVADOC ); sb.append( EOL ); // comment if ( StringUtils.isNotEmpty( entity.getComment() ) ) { updateJavadocComment( sb, originalContent, entity, indent ); } else { addDefaultJavadocComment( sb, entity, indent, isJavaMethod ); } // tags if ( entity.getTags() != null && entity.getTags().length > 0 ) { updateJavadocTags( sb, originalContent, entity, indent, isJavaMethod ); } else { addDefaultJavadocTags( sb, entity, indent, isJavaMethod ); } sb = new StringBuilder( removeLastEmptyJavadocLines( sb.toString() ) ).append( EOL ); sb.append( indent ).append( " " ).append( END_JAVADOC ); sb.append( EOL ); stringWriter.write( sb.toString() ); } /** * @param sb not null * @param originalContent not null * @param entity not null * @param indent not null * @throws IOException if any */ private void updateJavadocComment( final StringBuilder sb, final String originalContent, final AbstractInheritableJavaEntity entity, final String indent ) throws IOException { String comment = getJavadocComment( originalContent, entity ); comment = removeLastEmptyJavadocLines( comment ); comment = alignIndentationJavadocLines( comment, indent ); if ( comment.contains( START_JAVADOC ) ) { comment = comment.substring( comment.indexOf( START_JAVADOC ) + START_JAVADOC.length() ); comment = indent + SEPARATOR_JAVADOC + comment.trim(); } if ( comment.contains( END_JAVADOC ) ) { comment = comment.substring( 0, comment.indexOf( END_JAVADOC ) ); } if ( fixTag( LINK_TAG ) ) { comment = replaceLinkTags( comment, entity ); } String[] lines = getLines( comment ); for ( String line : lines ) { sb.append( indent ).append( " " ).append( line.trim() ); sb.append( EOL ); } } private static String replaceLinkTags( String comment, AbstractInheritableJavaEntity entity ) { StringBuilder resolvedComment = new StringBuilder(); // scan comment for {@link someClassName} and try to resolve this Matcher linktagMatcher = Pattern.compile( "\\{@link\\s" ).matcher( comment ); int startIndex = 0; while ( linktagMatcher.find() ) { int startName = linktagMatcher.end(); resolvedComment.append( comment.substring( startIndex, startName ) ); int endName = comment.indexOf( "}", startName ); if ( endName >= 0 ) { String name; String link = comment.substring( startName, endName ); int hashIndex = link.indexOf( '#' ); if ( hashIndex >= 0 ) { name = link.substring( 0, hashIndex ); } else { name = link; } if ( StringUtils.isNotBlank( name ) ) { String typeName; if ( entity instanceof JavaClass ) { typeName = ( (JavaClass) entity ).resolveType( name.trim() ); } else { typeName = entity.getParentClass().resolveType( name.trim() ); } if ( typeName == null ) { typeName = name.trim(); } else { typeName = typeName.replaceAll( "\\$", "." ); } //adjust name for inner classes resolvedComment.append( typeName ); } if ( hashIndex >= 0 ) { resolvedComment.append( link.substring( hashIndex ).trim() ); } startIndex = endName; } else { startIndex = startName; } } resolvedComment.append( comment.substring( startIndex ) ); return resolvedComment.toString(); } /** * @param sb not null * @param entity not null * @param indent not null * @param isJavaMethod */ private void addDefaultJavadocComment( final StringBuilder sb, final AbstractInheritableJavaEntity entity, final String indent, final boolean isJavaMethod ) { sb.append( indent ).append( SEPARATOR_JAVADOC ); if ( isJavaMethod ) { sb.append( getDefaultMethodJavadocComment( (JavaMethod) entity ) ); } else { sb.append( getDefaultClassJavadocComment( (JavaClass) entity ) ); } sb.append( EOL ); } /** * @param sb not null * @param originalContent not null * @param entity not null * @param indent not null * @param isJavaMethod * @throws IOException if any * @throws MojoExecutionException if any */ private void updateJavadocTags( final StringBuilder sb, final String originalContent, final AbstractInheritableJavaEntity entity, final String indent, final boolean isJavaMethod ) throws IOException, MojoExecutionException { appendSeparator( sb, indent ); // parse tags JavaEntityTags javaEntityTags = parseJavadocTags( originalContent, entity, indent, isJavaMethod ); // update and write tags updateJavadocTags( sb, entity, isJavaMethod, javaEntityTags ); // add missing tags... addMissingJavadocTags( sb, entity, indent, isJavaMethod, javaEntityTags ); } /** * Parse entity tags * * @param originalContent not null * @param entity not null * @param indent not null * @param isJavaMethod * @return an instance of {@link JavaEntityTags} * @throws IOException if any */ JavaEntityTags parseJavadocTags( final String originalContent, final AbstractInheritableJavaEntity entity, final String indent, final boolean isJavaMethod ) throws IOException { JavaEntityTags javaEntityTags = new JavaEntityTags( entity, isJavaMethod ); for ( int i = 0; i < entity.getTags().length; i++ ) { DocletTag docletTag = entity.getTags()[i]; String originalJavadocTag = getJavadocComment( originalContent, entity, docletTag ); originalJavadocTag = removeLastEmptyJavadocLines( originalJavadocTag ); originalJavadocTag = alignIndentationJavadocLines( originalJavadocTag, indent ); javaEntityTags.getNamesTags().add( docletTag.getName() ); if ( isJavaMethod ) { String[] params = docletTag.getParameters(); if ( params.length < 1 ) { continue; } params = fixQdox173( params ); String paramName = params[0]; if ( docletTag.getName().equals( PARAM_TAG ) ) { javaEntityTags.putJavadocParamTag( paramName, originalJavadocTag ); } else if ( docletTag.getName().equals( RETURN_TAG ) ) { javaEntityTags.setJavadocReturnTag( originalJavadocTag ); } else if ( docletTag.getName().equals( THROWS_TAG ) ) { javaEntityTags.putJavadocThrowsTag( paramName, originalJavadocTag ); } else { javaEntityTags.getUnknownTags().add( originalJavadocTag ); } } else { javaEntityTags.getUnknownTags().add( originalJavadocTag ); } } return javaEntityTags; } /** * Write tags according javaEntityTags. * * @param sb not null * @param entity not null * @param isJavaMethod * @param javaEntityTags not null */ private void updateJavadocTags( final StringBuilder sb, final AbstractInheritableJavaEntity entity, final boolean isJavaMethod, final JavaEntityTags javaEntityTags ) { for ( int i = 0; i < entity.getTags().length; i++ ) { DocletTag docletTag = entity.getTags()[i]; if ( isJavaMethod ) { JavaMethod javaMethod = (JavaMethod) entity; String[] params = docletTag.getParameters(); if ( params.length < 1 ) { continue; } if ( docletTag.getName().equals( PARAM_TAG ) ) { writeParamTag( sb, javaMethod, javaEntityTags, params ); } else if ( docletTag.getName().equals( RETURN_TAG ) ) { writeReturnTag( sb, javaMethod, javaEntityTags ); } else if ( docletTag.getName().equals( THROWS_TAG ) ) { writeThrowsTag( sb, javaMethod, javaEntityTags, params ); } else { // write unknown tags for ( Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); ) { String originalJavadocTag = it.next(); String simplified = StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim(); if ( simplified.contains( "@" + docletTag.getName() ) ) { it.remove(); sb.append( originalJavadocTag ); sb.append( EOL ); } } } } else { for ( Iterator<String> it = javaEntityTags.getUnknownTags().iterator(); it.hasNext(); ) { String originalJavadocTag = it.next(); String simplified = StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim(); if ( simplified.contains( "@" + docletTag.getName() ) ) { it.remove(); sb.append( originalJavadocTag ); sb.append( EOL ); } } } if ( sb.toString().endsWith( EOL ) ) { sb.delete( sb.toString().lastIndexOf( EOL ), sb.toString().length() ); } sb.append( EOL ); } } private void writeParamTag( final StringBuilder sb, final JavaMethod javaMethod, final JavaEntityTags javaEntityTags, String[] params ) { params = fixQdox173( params ); String paramName = params[0]; if ( !fixTag( PARAM_TAG ) ) { // write original param tag if found String originalJavadocTag = javaEntityTags.getJavadocParamTag( paramName ); if ( originalJavadocTag != null ) { sb.append( originalJavadocTag ); } return; } boolean found = false; JavaParameter javaParam = javaMethod.getParameterByName( paramName ); if ( javaParam == null ) { // is generic? TypeVariable[] typeParams = javaMethod.getTypeParameters(); for ( TypeVariable typeParam : typeParams ) { if ( typeParam.getGenericValue().equals( paramName ) ) { found = true; } } } else { found = true; } if ( !found ) { if ( getLog().isWarnEnabled() ) { getLog().warn( "Fixed unknown param '" + paramName + "' defined in " + getJavaMethodAsString( javaMethod ) ); } if ( sb.toString().endsWith( EOL ) ) { sb.delete( sb.toString().lastIndexOf( EOL ), sb.toString().length() ); } } else { String originalJavadocTag = javaEntityTags.getJavadocParamTag( paramName ); if ( originalJavadocTag != null ) { sb.append( originalJavadocTag ); String s = "@" + PARAM_TAG + " " + paramName; if ( StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim().endsWith( s ) ) { sb.append( " " ); sb.append( getDefaultJavadocForType( javaParam.getType() ) ); } } } } private void writeReturnTag( final StringBuilder sb, final JavaMethod javaMethod, final JavaEntityTags javaEntityTags ) { String originalJavadocTag = javaEntityTags.getJavadocReturnTag(); if ( originalJavadocTag == null ) { return; } if ( !fixTag( RETURN_TAG ) ) { // write original param tag if found sb.append( originalJavadocTag ); return; } if ( StringUtils.isNotEmpty( originalJavadocTag ) && javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() ) { sb.append( originalJavadocTag ); if ( originalJavadocTag.trim().endsWith( "@" + RETURN_TAG ) ) { sb.append( " " ); sb.append( getDefaultJavadocForType( javaMethod.getReturns() ) ); } } } void writeThrowsTag( final StringBuilder sb, final JavaMethod javaMethod, final JavaEntityTags javaEntityTags, final String[] params ) { String exceptionClassName = params[0]; String originalJavadocTag = javaEntityTags.getJavadocThrowsTag( exceptionClassName ); if ( originalJavadocTag == null ) { return; } if ( !fixTag( THROWS_TAG ) ) { // write original param tag if found sb.append( originalJavadocTag ); return; } if ( javaMethod.getExceptions() != null ) { for ( int j = 0; j < javaMethod.getExceptions().length; j++ ) { Type exception = javaMethod.getExceptions()[j]; if ( exception.getValue().endsWith( exceptionClassName ) ) { originalJavadocTag = StringUtils.replace( originalJavadocTag, exceptionClassName, exception.getValue() ); if ( StringUtils.removeDuplicateWhitespace( originalJavadocTag ).trim().endsWith( "@" + THROWS_TAG + " " + exception.getValue() ) ) { originalJavadocTag += " if any."; } sb.append( originalJavadocTag ); // added qualified name javaEntityTags.putJavadocThrowsTag( exception.getValue(), originalJavadocTag ); return; } } } Class<?> clazz = getClass( javaMethod.getParentClass(), exceptionClassName ); if ( clazz != null ) { if ( ClassUtils.isAssignable( clazz, RuntimeException.class ) ) { sb.append( StringUtils.replace( originalJavadocTag, exceptionClassName, clazz.getName() ) ); // added qualified name javaEntityTags.putJavadocThrowsTag( clazz.getName(), originalJavadocTag ); } else if ( ClassUtils.isAssignable( clazz, Throwable.class ) ) { getLog().debug( "Removing '" + originalJavadocTag + "'; Throwable not specified by " + getJavaMethodAsString( javaMethod ) + " and it is not a RuntimeException." ); } else { getLog().debug( "Removing '" + originalJavadocTag + "'; It is not a Throwable" ); } } else if ( removeUnknownThrows ) { getLog().warn( "Ignoring unknown throws '" + exceptionClassName + "' defined on " + getJavaMethodAsString( javaMethod ) ); } else { getLog().warn( "Found unknown throws '" + exceptionClassName + "' defined on " + getJavaMethodAsString( javaMethod ) ); sb.append( originalJavadocTag ); if ( params.length == 1 ) { sb.append( " if any." ); } javaEntityTags.putJavadocThrowsTag( exceptionClassName, originalJavadocTag ); } } /** * Add missing tags not already written. * * @param sb not null * @param entity not null * @param indent not null * @param isJavaMethod * @param javaEntityTags not null * @throws MojoExecutionException if any */ private void addMissingJavadocTags( final StringBuilder sb, final AbstractInheritableJavaEntity entity, final String indent, final boolean isJavaMethod, final JavaEntityTags javaEntityTags ) throws MojoExecutionException { if ( isJavaMethod ) { JavaMethod javaMethod = (JavaMethod) entity; if ( fixTag( PARAM_TAG ) ) { if ( javaMethod.getParameters() != null ) { for ( int i = 0; i < javaMethod.getParameters().length; i++ ) { JavaParameter javaParameter = javaMethod.getParameters()[i]; if ( javaEntityTags.getJavadocParamTag( javaParameter.getName(), true ) == null ) { appendDefaultParamTag( sb, indent, javaParameter ); } } } // is generic? if ( javaMethod.getTypeParameters() != null ) { for ( int i = 0; i < javaMethod.getTypeParameters().length; i++ ) { TypeVariable typeParam = javaMethod.getTypeParameters()[i]; if ( javaEntityTags.getJavadocParamTag( "<" + typeParam.getName() + ">", true ) == null ) { appendDefaultParamTag( sb, indent, typeParam ); } } } } if ( fixTag( RETURN_TAG ) && StringUtils.isEmpty( javaEntityTags.getJavadocReturnTag() ) && javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() ) { appendDefaultReturnTag( sb, indent, javaMethod ); } if ( fixTag( THROWS_TAG ) && javaMethod.getExceptions() != null ) { for ( int i = 0; i < javaMethod.getExceptions().length; i++ ) { Type exception = javaMethod.getExceptions()[i]; if ( javaEntityTags.getJavadocThrowsTag( exception.getValue(), true ) == null ) { appendDefaultThrowsTag( sb, indent, exception ); } } } } else { if ( !javaEntityTags.getNamesTags().contains( AUTHOR_TAG ) ) { appendDefaultAuthorTag( sb, indent ); } if ( !javaEntityTags.getNamesTags().contains( VERSION_TAG ) ) { appendDefaultVersionTag( sb, indent ); } } if ( fixTag( SINCE_TAG ) && !javaEntityTags.getNamesTags().contains( SINCE_TAG ) ) { if ( !isJavaMethod ) { if ( !ignoreClirr ) { if ( isNewClassFromLastVersion( (JavaClass) entity ) ) { appendDefaultSinceTag( sb, indent ); } } else { appendDefaultSinceTag( sb, indent ); addSinceClasses( (JavaClass) entity ); } } else { if ( !ignoreClirr ) { if ( isNewMethodFromLastRevision( (JavaMethod) entity ) ) { appendDefaultSinceTag( sb, indent ); } } else { if ( sinceClasses != null && !sinceClassesContains( entity.getParentClass() ) ) { appendDefaultSinceTag( sb, indent ); } } } } } /** * @param sb not null * @param entity not null * @param indent not null * @param isJavaMethod * @throws MojoExecutionException if any */ private void addDefaultJavadocTags( final StringBuilder sb, final AbstractInheritableJavaEntity entity, final String indent, final boolean isJavaMethod ) throws MojoExecutionException { boolean separatorAdded = false; if ( isJavaMethod ) { JavaMethod javaMethod = (JavaMethod) entity; if ( fixTag( PARAM_TAG ) && javaMethod.getParameters() != null ) { for ( int i = 0; i < javaMethod.getParameters().length; i++ ) { JavaParameter javaParameter = javaMethod.getParameters()[i]; separatorAdded = appendDefaultParamTag( sb, indent, separatorAdded, javaParameter ); } } if ( fixTag( RETURN_TAG ) ) { if ( javaMethod.getReturns() != null && !javaMethod.getReturns().isVoid() ) { separatorAdded = appendDefaultReturnTag( sb, indent, separatorAdded, javaMethod ); } } if ( fixTag( THROWS_TAG ) && javaMethod.getExceptions() != null ) { for ( int i = 0; i < javaMethod.getExceptions().length; i++ ) { Type exception = javaMethod.getExceptions()[i]; separatorAdded = appendDefaultThrowsTag( sb, indent, separatorAdded, exception ); } } } else { separatorAdded = appendDefaultAuthorTag( sb, indent, separatorAdded ); separatorAdded = appendDefaultVersionTag( sb, indent, separatorAdded ); } if ( fixTag( SINCE_TAG ) ) { if ( !isJavaMethod ) { JavaClass javaClass = (JavaClass) entity; if ( !ignoreClirr ) { if ( isNewClassFromLastVersion( javaClass ) ) { separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded ); } } else { separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded ); addSinceClasses( javaClass ); } } else { JavaMethod javaMethod = (JavaMethod) entity; if ( !ignoreClirr ) { if ( isNewMethodFromLastRevision( javaMethod ) ) { separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded ); } } else { if ( sinceClasses != null && !sinceClassesContains( javaMethod.getParentClass() ) ) { separatorAdded = appendDefaultSinceTag( sb, indent, separatorAdded ); } } } } } /** * @param sb not null * @param indent not null * @param separatorAdded * @return true if separator has been added. */ private boolean appendDefaultAuthorTag( final StringBuilder sb, final String indent, boolean separatorAdded ) { if ( !fixTag( AUTHOR_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultAuthorTag( sb, indent ); return separatorAdded; } /** * @param sb not null * @param indent not null */ private void appendDefaultAuthorTag( final StringBuilder sb, final String indent ) { if ( !fixTag( AUTHOR_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( AUTHOR_TAG ).append( " " ); sb.append( defaultAuthor ); sb.append( EOL ); } /** * @param sb not null * @param indent not null * @param separatorAdded * @return true if separator has been added. */ private boolean appendDefaultSinceTag( final StringBuilder sb, final String indent, boolean separatorAdded ) { if ( !fixTag( SINCE_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultSinceTag( sb, indent ); return separatorAdded; } /** * @param sb not null * @param indent not null */ private void appendDefaultSinceTag( final StringBuilder sb, final String indent ) { if ( !fixTag( SINCE_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( SINCE_TAG ).append( " " ); sb.append( defaultSince ); sb.append( EOL ); } /** * @param sb not null * @param indent not null * @param separatorAdded * @return true if separator has been added. */ private boolean appendDefaultVersionTag( final StringBuilder sb, final String indent, boolean separatorAdded ) { if ( !fixTag( VERSION_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultVersionTag( sb, indent ); return separatorAdded; } /** * @param sb not null * @param indent not null */ private void appendDefaultVersionTag( final StringBuilder sb, final String indent ) { if ( !fixTag( VERSION_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( VERSION_TAG ).append( " " ); sb.append( defaultVersion ); sb.append( EOL ); } /** * @param sb not null * @param indent not null * @param separatorAdded * @param javaParameter not null * @return true if separator has been added. */ private boolean appendDefaultParamTag( final StringBuilder sb, final String indent, boolean separatorAdded, final JavaParameter javaParameter ) { if ( !fixTag( PARAM_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultParamTag( sb, indent, javaParameter ); return separatorAdded; } /** * @param sb not null * @param indent not null * @param separatorAdded * @param typeParameter not null * @return true if separator has been added. */ private boolean appendDefaultParamTag( final StringBuilder sb, final String indent, boolean separatorAdded, final TypeVariable typeParameter ) { if ( !fixTag( PARAM_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultParamTag( sb, indent, typeParameter ); return separatorAdded; } /** * @param sb not null * @param indent not null * @param javaParameter not null */ private void appendDefaultParamTag( final StringBuilder sb, final String indent, final JavaParameter javaParameter ) { if ( !fixTag( PARAM_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( PARAM_TAG ).append( " " ); sb.append( javaParameter.getName() ); sb.append( " " ); sb.append( getDefaultJavadocForType( javaParameter.getType() ) ); sb.append( EOL ); } /** * @param sb not null * @param indent not null * @param typeParameter not null */ private void appendDefaultParamTag( final StringBuilder sb, final String indent, final TypeVariable typeParameter ) { if ( !fixTag( PARAM_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( PARAM_TAG ).append( " " ); sb.append( "<" ).append( typeParameter.getName() ).append( ">" ); sb.append( " " ); sb.append( getDefaultJavadocForType( typeParameter ) ); sb.append( EOL ); } /** * @param sb not null * @param indent not null * @param separatorAdded * @param javaMethod not null * @return true if separator has been added. */ private boolean appendDefaultReturnTag( final StringBuilder sb, final String indent, boolean separatorAdded, final JavaMethod javaMethod ) { if ( !fixTag( RETURN_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultReturnTag( sb, indent, javaMethod ); return separatorAdded; } /** * @param sb not null * @param indent not null * @param javaMethod not null */ private void appendDefaultReturnTag( final StringBuilder sb, final String indent, final JavaMethod javaMethod ) { if ( !fixTag( RETURN_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( RETURN_TAG ).append( " " ); sb.append( getDefaultJavadocForType( javaMethod.getReturns() ) ); sb.append( EOL ); } /** * @param sb not null * @param indent not null * @param separatorAdded * @param exception not null * @return true if separator has been added. */ private boolean appendDefaultThrowsTag( final StringBuilder sb, final String indent, boolean separatorAdded, final Type exception ) { if ( !fixTag( THROWS_TAG ) ) { return separatorAdded; } if ( !separatorAdded ) { appendSeparator( sb, indent ); separatorAdded = true; } appendDefaultThrowsTag( sb, indent, exception ); return separatorAdded; } /** * @param sb not null * @param indent not null * @param exception not null */ private void appendDefaultThrowsTag( final StringBuilder sb, final String indent, final Type exception ) { if ( !fixTag( THROWS_TAG ) ) { return; } sb.append( indent ).append( " * @" ).append( THROWS_TAG ).append( " " ); sb.append( exception.getJavaClass().getFullyQualifiedName() ); sb.append( " if any." ); sb.append( EOL ); } /** * @param sb not null * @param indent not null */ private void appendSeparator( final StringBuilder sb, final String indent ) { sb.append( indent ).append( " *" ); sb.append( EOL ); } /** * Verify if a method has <code>@java.lang.Override()</code> annotation or if it is an inherited method * from an interface or a super class. The goal is to handle <code>{@inheritDoc}</code> tag. * * @param javaMethod not null * @return <code>true</code> if the method is inherited, <code>false</code> otherwise. * @throws MojoExecutionException if any */ private boolean isInherited( JavaMethod javaMethod ) throws MojoExecutionException { if ( javaMethod.getAnnotations() != null ) { for ( int i = 0; i < javaMethod.getAnnotations().length; i++ ) { Annotation annotation = javaMethod.getAnnotations()[i]; if ( annotation.toString().equals( "@java.lang.Override()" ) ) { return true; } } } Class<?> clazz = getClass( javaMethod.getParentClass().getFullyQualifiedName() ); List<Class<?>> interfaces = ClassUtils.getAllInterfaces( clazz ); for ( Class<?> intface : interfaces ) { if ( isInherited( intface, javaMethod ) ) { return true; } } List<Class<?>> classes = ClassUtils.getAllSuperclasses( clazz ); for ( Class<?> superClass : classes ) { if ( isInherited( superClass, javaMethod ) ) { return true; } } return false; } /** * @param clazz the Java class object, not null * @param javaMethod the QDox JavaMethod object not null * @return <code>true</code> if <code>javaMethod</code> exists in the given <code>clazz</code>, * <code>false</code> otherwise. * @see #isInherited(JavaMethod) */ private boolean isInherited( Class<?> clazz, JavaMethod javaMethod ) { for ( Method method : clazz.getDeclaredMethods() ) { if ( !method.getName().equals( javaMethod.getName() ) ) { continue; } if ( method.getParameterTypes().length != javaMethod.getParameters().length ) { continue; } boolean found = false; int j = 0; for ( Class<?> paramType : method.getParameterTypes() ) { String name1 = paramType.getName(); String name2 = javaMethod.getParameters()[j++].getType().getFullQualifiedName(); found = name1.equals( name2 ); // TODO check algo, seems broken (only takes in account the last param) } return found; } return false; } /** * @param type * @return */ private String getDefaultJavadocForType( Type type ) { StringBuilder sb = new StringBuilder(); if ( !TypeVariable.class.isAssignableFrom( type.getClass() ) && type.isPrimitive() ) { if ( type.isArray() ) { sb.append( "an array of " ); } else { sb.append( "a " ); } return sb.append( type.getJavaClass().getFullyQualifiedName() ).append( "." ).toString(); } StringBuilder javadocLink = new StringBuilder(); try { getClass( type.getJavaClass().getFullyQualifiedName() ); String s = type.getJavaClass().getFullyQualifiedName(); s = StringUtils.replace( s, "$", "." ); javadocLink.append( "{@link " ).append( s ).append( "}" ); } catch ( Exception e ) { javadocLink.append( type.getJavaClass().getFullyQualifiedName() ); } if ( type.isArray() ) { sb.append( "an array of " ).append( javadocLink.toString() ).append( " objects." ); } else { sb.append( "a " ).append( javadocLink.toString() ).append( " object." ); } return sb.toString(); } /** * Check under Clirr if this given class is newer from the last version. * * @param javaClass a given class not null * @return <code>true</code> if Clirr said that this class is added from the last version, * <code>false</code> otherwise or if {@link #clirrNewClasses} is null. */ private boolean isNewClassFromLastVersion( JavaClass javaClass ) { return ( clirrNewClasses != null ) && clirrNewClasses.contains( javaClass.getFullyQualifiedName() ); } /** * Check under Clirr if this given method is newer from the last version. * * @param javaMethod a given method not null * @return <code>true</code> if Clirr said that this method is added from the last version, * <code>false</code> otherwise or if {@link #clirrNewMethods} is null. * @throws MojoExecutionException if any */ private boolean isNewMethodFromLastRevision( JavaMethod javaMethod ) throws MojoExecutionException { if ( clirrNewMethods == null ) { return false; } List<String> clirrMethods = clirrNewMethods.get( javaMethod.getParentClass().getFullyQualifiedName() ); if ( clirrMethods == null ) { return false; } for ( String clirrMethod : clirrMethods ) { // see net.sf.clirr.core.internal.checks.MethodSetCheck#getMethodId(JavaType clazz, Method method) String retrn = ""; if ( javaMethod.getReturns() != null ) { retrn = javaMethod.getReturns().getFullQualifiedName(); } StringBuilder params = new StringBuilder(); for ( JavaParameter parameter : javaMethod.getParameters() ) { if ( params.length() > 0 ) { params.append( ", " ); } params.append( parameter.getResolvedValue() ); } if ( clirrMethod.contains( retrn + " " ) && clirrMethod.contains( javaMethod.getName() + "(" ) && clirrMethod.contains( "(" + params.toString() + ")" ) ) { return true; } } return false; } /** * @param className not null * @return the Class corresponding to the given class name using the project classloader. * @throws MojoExecutionException if class not found * @see {@link ClassUtils#getClass(ClassLoader, String, boolean)} * @see {@link #getProjectClassLoader()} */ private Class<?> getClass( String className ) throws MojoExecutionException { try { return ClassUtils.getClass( getProjectClassLoader(), className, false ); } catch ( ClassNotFoundException e ) { throw new MojoExecutionException( "ClassNotFoundException: " + e.getMessage(), e ); } } /** * Returns the Class object assignable for {@link RuntimeException} class and associated with the given * exception class name. * * @param currentClass not null * @param exceptionClassName not null, an exception class name defined as: * <ul> * <li>exception class fully qualified</li> * <li>exception class in the same package</li> * <li>exception inner class</li> * <li>exception class in java.lang package</li> * </ul> * @return the class if found, otherwise {@code null}. * @see #getClass(String) */ private Class<?> getClass( JavaClass currentClass, String exceptionClassName ) { String[] potentialClassNames = new String[]{ exceptionClassName, currentClass.getPackage().getName() + "." + exceptionClassName, currentClass.getPackage().getName() + "." + currentClass.getName() + "$" + exceptionClassName, "java.lang." + exceptionClassName }; Class<?> clazz = null; for ( String potentialClassName : potentialClassNames ) { try { clazz = getClass( potentialClassName ); } catch ( MojoExecutionException e ) { // nop } if ( clazz != null ) { return clazz; } } return null; } /** * @param javaClass not null */ private void addSinceClasses( JavaClass javaClass ) { if ( sinceClasses == null ) { sinceClasses = new ArrayList<String>(); } sinceClasses.add( javaClass.getFullyQualifiedName() ); } private boolean sinceClassesContains( JavaClass javaClass ) { return sinceClasses.contains( javaClass.getFullyQualifiedName() ); } // ---------------------------------------------------------------------- // Static methods // ---------------------------------------------------------------------- /** * Write content into the given javaFile and using the given encoding. * All line separators will be unified. * * @param javaFile not null * @param encoding not null * @param content not null * @throws IOException if any */ private static void writeFile( final File javaFile, final String encoding, final String content ) throws IOException { Writer writer = null; try { writer = WriterFactory.newWriter( javaFile, encoding ); writer.write( StringUtils.unifyLineSeparators( content ) ); writer.close(); writer = null; } finally { IOUtil.close( writer ); } } /** * @return the full clirr goal, i.e. <code>groupId:artifactId:version:goal</code>. The clirr-plugin version * could be load from the pom.properties in the clirr-maven-plugin dependency. */ private static String getFullClirrGoal() { StringBuilder sb = new StringBuilder(); sb.append( CLIRR_MAVEN_PLUGIN_GROUPID ).append( ":" ).append( CLIRR_MAVEN_PLUGIN_ARTIFACTID ).append( ":" ); String clirrVersion = CLIRR_MAVEN_PLUGIN_VERSION; InputStream resourceAsStream = null; try { String resource = "META-INF/maven/" + CLIRR_MAVEN_PLUGIN_GROUPID + "/" + CLIRR_MAVEN_PLUGIN_ARTIFACTID + "/pom.properties"; resourceAsStream = AbstractFixJavadocMojo.class.getClassLoader().getResourceAsStream( resource ); if ( resourceAsStream != null ) { Properties properties = new Properties(); properties.load( resourceAsStream ); resourceAsStream.close(); resourceAsStream = null; if ( StringUtils.isNotEmpty( properties.getProperty( "version" ) ) ) { clirrVersion = properties.getProperty( "version" ); } } } catch ( IOException e ) { // nop } finally { IOUtil.close( resourceAsStream ); } sb.append( clirrVersion ).append( ":" ).append( CLIRR_MAVEN_PLUGIN_GOAL ); return sb.toString(); } /** * Default comment for class. * * @param javaClass not null * @return a default comment for class. */ private static String getDefaultClassJavadocComment( final JavaClass javaClass ) { StringBuilder sb = new StringBuilder(); sb.append( "<p>" ); if ( Arrays.asList( javaClass.getModifiers() ).contains( "abstract" ) ) { sb.append( "Abstract " ); } sb.append( javaClass.getName() ); if ( !javaClass.isInterface() ) { sb.append( " class." ); } else { sb.append( " interface." ); } sb.append( "</p>" ); return sb.toString(); } /** * Default comment for method with taking care of getter/setter in the javaMethod name. * * @param javaMethod not null * @return a default comment for method. */ private static String getDefaultMethodJavadocComment( final JavaMethod javaMethod ) { if ( javaMethod.isConstructor() ) { return "<p>Constructor for " + javaMethod.getName() + ".</p>"; } if ( javaMethod.getName().length() > 3 && ( javaMethod.getName().startsWith( "get" ) || javaMethod.getName().startsWith( "set" ) ) ) { String field = StringUtils.lowercaseFirstLetter( javaMethod.getName().substring( 3 ) ); JavaClass clazz = javaMethod.getParentClass(); if ( clazz.getFieldByName( field ) == null ) { return "<p>" + javaMethod.getName() + ".</p>"; } StringBuilder sb = new StringBuilder(); sb.append( "<p>" ); if ( javaMethod.getName().startsWith( "get" ) ) { sb.append( "Getter " ); } else if ( javaMethod.getName().startsWith( "set" ) ) { sb.append( "Setter " ); } sb.append( "for the field <code>" ).append( field ).append( "</code>.</p>" ); return sb.toString(); } return "<p>" + javaMethod.getName() + ".</p>"; } /** * Try to find if a Javadoc comment has an {@link #INHERITED_TAG} for instance: * <pre> * /** {@inheritDoc} */ * </pre> * or * <pre> * /** * * {@inheritDoc} * */ * </pre> * * @param content not null * @return <code>true</code> if the content has an inherited tag, <code>false</code> otherwise. */ private static boolean hasInheritedTag( final String content ) { final String inheritedTagPattern = "^\\s*(\\/\\*\\*)?(\\s*(\\*)?)*(\\{)@inheritDoc\\s*(\\})(\\s*(\\*)?)*(\\*\\/)?$"; return Pattern.matches( inheritedTagPattern, StringUtils.removeDuplicateWhitespace( content ) ); } /** * Workaround for QDOX-146 about whitespace. * Ideally we want to use <code>entity.getComment()</code> * <br/> * For instance, with the following snippet: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff"></font><br /> * <font color="#808080">2</font> <font color="#ffffff">    </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">3</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * <font color="#808080">4</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * <font color="#808080">5</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">6</font> <font color="#ffffff">    </font> * <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> * <font color="#000000">dummyMethod</font><font color="#000000">( </font> * <font color="#000000">String s </font><font color="#000000">){}</font><br /> * </code> * <p/> * <br/> * The return will be: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * </code> * * @param javaClassContent original class content not null * @param entity not null * @return the javadoc comment for the entity without any tags. * @throws IOException if any */ private static String getJavadocComment( final String javaClassContent, final AbstractJavaEntity entity ) throws IOException { if ( entity.getComment() == null ) { return ""; } String originalJavadoc = extractOriginalJavadocContent( javaClassContent, entity ); StringBuilder sb = new StringBuilder(); BufferedReader lr = new BufferedReader( new StringReader( originalJavadoc ) ); String line; while ( ( line = lr.readLine() ) != null ) { String l = StringUtils.removeDuplicateWhitespace( line.trim() ); if ( l.startsWith( "* @" ) || l.startsWith( "*@" ) ) { break; } sb.append( line ).append( EOL ); } return trimRight( sb.toString() ); } /** * Work around for QDOX-146 about whitespace. * Ideally we want to use <code>docletTag.getValue()</code> * <br/> * For instance, with the following snippet: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff"></font><br /> * <font color="#808080">2</font> <font color="#ffffff">    </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">3</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * <font color="#808080">4</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * <font color="#808080">5</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">6</font> <font color="#ffffff">    </font> * <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> * <font color="#000000">dummyMethod</font><font color="#000000">( </font> * <font color="#000000">String s </font><font color="#000000">){}</font><br /> * </code> * <p/> * <br/> * The return will be: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * </code> * * @param javaClassContent original class content not null * @param entity not null * @param docletTag not null * @return the javadoc comment for the entity without Javadoc tags. * @throws IOException if any */ private String getJavadocComment( final String javaClassContent, final AbstractInheritableJavaEntity entity, final DocletTag docletTag ) throws IOException { if ( docletTag.getValue() == null || docletTag.getParameters().length == 0 ) { return ""; } String originalJavadoc = extractOriginalJavadocContent( javaClassContent, entity ); String[] params = fixQdox173( docletTag.getParameters() ); String paramValue = params[0]; StringBuilder sb = new StringBuilder(); BufferedReader lr = new BufferedReader( new StringReader( originalJavadoc ) ); String line; boolean found = false; while ( ( line = lr.readLine() ) != null ) { String l = StringUtils.removeDuplicateWhitespace( line.trim() ); if ( l.startsWith( "* @" + docletTag.getName() + " " + paramValue ) || l.startsWith( "*@" + docletTag.getName() + " " + paramValue ) ) { if ( fixTag( LINK_TAG ) ) { line = replaceLinkTags( line, entity ); } sb.append( line ).append( EOL ); found = true; } else { if ( l.startsWith( "* @" ) || l.startsWith( "*@" ) ) { found = false; } if ( found ) { if ( fixTag( LINK_TAG ) ) { line = replaceLinkTags( line, entity ); } sb.append( line ).append( EOL ); } } } return trimRight( sb.toString() ); } /** * Extract the original Javadoc and others comments up to {@link #START_JAVADOC} form the entity. This method * takes care of the Javadoc indentation. All javadoc lines will be trimmed on right. * <br/> * For instance, with the following snippet: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff"></font><br /> * <font color="#808080">2</font> <font color="#ffffff">    </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">3</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * <font color="#808080">4</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * <font color="#808080">5</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">6</font> <font color="#ffffff">    </font> * <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> * <font color="#000000">dummyMethod</font><font color="#000000">( </font> * <font color="#000000">String s </font><font color="#000000">){}</font><br /> * </code> * <p/> * <br/> * The return will be: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff">    </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">2</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * <font color="#808080">3</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * <font color="#808080">4</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">*/</font><br /> * </code> * * @param javaClassContent not null * @param entity not null * @return return the original javadoc as String for the current entity * @throws IOException if any */ private static String extractOriginalJavadoc( final String javaClassContent, final AbstractJavaEntity entity ) throws IOException { if ( entity.getComment() == null ) { return ""; } String[] javaClassContentLines = getLines( javaClassContent ); List<String> list = new LinkedList<String>(); for ( int i = entity.getLineNumber() - 2; i >= 0; i-- ) { String line = javaClassContentLines[i]; list.add( trimRight( line ) ); if ( line.trim().startsWith( START_JAVADOC ) ) { break; } } Collections.reverse( list ); return StringUtils.join( list.iterator(), EOL ); } /** * Extract the Javadoc comment between {@link #START_JAVADOC} and {@link #END_JAVADOC} form the entity. This method * takes care of the Javadoc indentation. All javadoc lines will be trimmed on right. * <br/> * For instance, with the following snippet: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff"></font><br /> * <font color="#808080">2</font> <font color="#ffffff">    </font> * <font color="#3f5fbf">/**</font><br /> * <font color="#808080">3</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * <font color="#808080">4</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * <font color="#808080">5</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">*/</font><br /> * <font color="#808080">6</font> <font color="#ffffff">    </font> * <font color="#7f0055"><b>public </b></font><font color="#7f0055"><b>void </b></font> * <font color="#000000">dummyMethod</font><font color="#000000">( </font> * <font color="#000000">String s </font><font color="#000000">){}</font><br /> * </code> * <p/> * <br/> * The return will be: * <br/> * <p/> * <code> * <font color="#808080">1</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* Dummy Javadoc comment.</font><br /> * <font color="#808080">2</font> <font color="#ffffff">     </font> * <font color="#3f5fbf">* </font><font color="#7f9fbf">@param </font> * <font color="#3f5fbf">s a String</font><br /> * </code> * * @param javaClassContent not null * @param entity not null * @return return the original javadoc as String for the current entity * @throws IOException if any */ private static String extractOriginalJavadocContent( final String javaClassContent, final AbstractJavaEntity entity ) throws IOException { if ( entity.getComment() == null ) { return ""; } String originalJavadoc = extractOriginalJavadoc( javaClassContent, entity ); int index = originalJavadoc.indexOf( START_JAVADOC ); if ( index != -1 ) { originalJavadoc = originalJavadoc.substring( index + START_JAVADOC.length() ); } index = originalJavadoc.indexOf( END_JAVADOC ); if ( index != -1 ) { originalJavadoc = originalJavadoc.substring( 0, index ); } if ( originalJavadoc.startsWith( "\r\n" ) ) { originalJavadoc = originalJavadoc.substring( 2 ); } else if ( originalJavadoc.startsWith( "\n" ) || originalJavadoc.startsWith( "\r" ) ) { originalJavadoc = originalJavadoc.substring( 1 ); } return trimRight( originalJavadoc ); } /** * @param content not null * @return the content without last lines containing javadoc separator (ie <code> * </code>) * @throws IOException if any * @see #getJavadocComment(String, AbstractInheritableJavaEntity, DocletTag) */ private static String removeLastEmptyJavadocLines( final String content ) throws IOException { if ( !content.contains( EOL ) ) { return content; } String[] lines = getLines( content ); if ( lines.length == 1 ) { return content; } List<String> linesList = new LinkedList<String>( Arrays.asList( lines ) ); Collections.reverse( linesList ); for ( Iterator<String> it = linesList.iterator(); it.hasNext(); ) { String line = it.next(); if ( line.trim().equals( "*" ) ) { it.remove(); } else { break; } } Collections.reverse( linesList ); return StringUtils.join( linesList.iterator(), EOL ); } /** * @param content not null * @return the javadoc comment with the given indentation * @throws IOException if any * @see #getJavadocComment(String, AbstractInheritableJavaEntity, DocletTag) */ private static String alignIndentationJavadocLines( final String content, final String indent ) throws IOException { StringBuilder sb = new StringBuilder(); for ( String line : getLines( content ) ) { if ( sb.length() > 0 ) { sb.append( EOL ); } if ( !line.trim().startsWith( "*" ) ) { line = "*" + line; } sb.append( indent ).append( " " ).append( trimLeft( line ) ); } return sb.toString(); } /** * Autodetect the indentation of a given line: * <pre> * autodetectIndentation( null ) = ""; * autodetectIndentation( "a" ) = ""; * autodetectIndentation( " a" ) = " "; * autodetectIndentation( "\ta" ) = "\t"; * </pre> * * @param line not null * @return the indentation for the given line. */ private static String autodetectIndentation( final String line ) { if ( StringUtils.isEmpty( line ) ) { return ""; } return line.substring( 0, line.indexOf( trimLeft( line ) ) ); } /** * @param content not null * @return an array of all content lines * @throws IOException if any */ private static String[] getLines( final String content ) throws IOException { List<String> lines = new LinkedList<String>(); BufferedReader reader = new BufferedReader( new StringReader( content ) ); String line = reader.readLine(); while ( line != null ) { lines.add( line ); line = reader.readLine(); } return lines.toArray( new String[lines.size()] ); } /** * Trim a given line on the left: * <pre> * trimLeft( null ) = ""; * trimLeft( " " ) = ""; * trimLeft( "a" ) = "a"; * trimLeft( " a" ) = "a"; * trimLeft( "\ta" ) = "a"; * trimLeft( " a " ) = "a "; * </pre> * * @param text * @return the text trimmed on left side or empty if text is null. */ private static String trimLeft( final String text ) { if ( StringUtils.isEmpty( text ) || StringUtils.isEmpty( text.trim() ) ) { return ""; } String textTrimmed = text.trim(); return text.substring( text.indexOf( textTrimmed ), text.length() ); } /** * Trim a given line on the right: * <pre> * trimRight( null ) = ""; * trimRight( " " ) = ""; * trimRight( "a" ) = "a"; * trimRight( "a\t" ) = "a"; * trimRight( " a " ) = " a"; * </pre> * * @param text * @return the text trimmed on tight side or empty if text is null. */ private static String trimRight( final String text ) { if ( StringUtils.isEmpty( text ) || StringUtils.isEmpty( text.trim() ) ) { return ""; } String textTrimmed = text.trim(); return text.substring( 0, text.indexOf( textTrimmed ) + textTrimmed.length() ); } /** * Workaroung for QDOX-173 about generic. * * @param params not null * @return the wanted params. */ private static String[] fixQdox173( String[] params ) { if ( params == null || params.length == 0 || params.length < 3 ) { return params; } if ( params[0].trim().equals( "<" ) && params[2].trim().equals( ">" ) ) { String param = params[1]; List<String> l = new ArrayList<String>( Arrays.asList( params ) ); l.set( 1, "<" + param + ">" ); l.remove( 0 ); l.remove( 1 ); return l.toArray( new String[l.size()] ); } return params; } /** * Wrapper class for the entity's tags. */ class JavaEntityTags { private final AbstractInheritableJavaEntity entity; private final boolean isJavaMethod; /** * List of tag names. */ private List<String> namesTags; /** * Map with java parameter as key and original Javadoc lines as values. */ private Map<String, String> tagParams; /** * Original javadoc lines. */ private String tagReturn; /** * Map with java throw as key and original Javadoc lines as values. */ private Map<String, String> tagThrows; /** * Original javadoc lines for unknown tags. */ private List<String> unknownsTags; public JavaEntityTags( AbstractInheritableJavaEntity entity, boolean isJavaMethod ) { this.entity = entity; this.isJavaMethod = isJavaMethod; this.namesTags = new LinkedList<String>(); this.tagParams = new LinkedHashMap<String, String>(); this.tagThrows = new LinkedHashMap<String, String>(); this.unknownsTags = new LinkedList<String>(); } public List<String> getNamesTags() { return namesTags; } public String getJavadocReturnTag() { return tagReturn; } public void setJavadocReturnTag( String s ) { tagReturn = s; } public List<String> getUnknownTags() { return unknownsTags; } public void putJavadocParamTag( String paramName, String originalJavadocTag ) { tagParams.put( paramName, originalJavadocTag ); } public String getJavadocParamTag( String paramName ) { return getJavadocParamTag( paramName, false ); } public String getJavadocParamTag( String paramName, boolean nullable ) { String originalJavadocTag = tagParams.get( paramName ); if ( !nullable && originalJavadocTag == null && getLog().isWarnEnabled() ) { getLog().warn( getMessage( paramName, "javaEntityTags.tagParams" ) ); } return originalJavadocTag; } public void putJavadocThrowsTag( String paramName, String originalJavadocTag ) { tagThrows.put( paramName, originalJavadocTag ); } public String getJavadocThrowsTag( String paramName ) { return getJavadocThrowsTag( paramName, false ); } public String getJavadocThrowsTag( String paramName, boolean nullable ) { String originalJavadocTag = tagThrows.get( paramName ); if ( !nullable && originalJavadocTag == null && getLog().isWarnEnabled() ) { getLog().warn( getMessage( paramName, "javaEntityTags.tagThrows" ) ); } return originalJavadocTag; } private String getMessage( String paramName, String mapName ) { StringBuilder msg = new StringBuilder(); msg.append( "No param '" ).append( paramName ).append( "' key found in " ).append( mapName ) .append( " for the entity: " ); if ( isJavaMethod ) { JavaMethod javaMethod = (JavaMethod) entity; msg.append( getJavaMethodAsString( javaMethod ) ); } else { JavaClass javaClass = (JavaClass) entity; msg.append( javaClass.getFullyQualifiedName() ); } return msg.toString(); } /** * {@inheritDoc} */ public String toString() { StringBuilder sb = new StringBuilder(); sb.append( "namesTags=" ).append( namesTags ).append( "\n" ); sb.append( "tagParams=" ).append( tagParams ).append( "\n" ); sb.append( "tagReturn=" ).append( tagReturn ).append( "\n" ); sb.append( "tagThrows=" ).append( tagThrows ).append( "\n" ); sb.append( "unknownsTags=" ).append( unknownsTags ).append( "\n" ); return sb.toString(); } } }